﻿define([
    'Wjs/core/array',
    'Wjs/core/types',
    'Wjs/core/typeManager'
],
function (array, types, typeManager) {
    'use strict';

    var type = 'ClassBuilder',
        ClassBuilder = typeManager.getType(type, true /*optional*/),
        PropertyBuilder,
        DataPropertyBuilder,
        AccessorPropertyBuilder;

    if (ClassBuilder) { // It has been already loaded

        return ClassBuilder;
    }

    var extend = function (derived, base) {

            derived.prototype = Object.create(base.prototype || base || {});
        },

        getAllMixins = function (classBuilder, output) {
            var mixins = classBuilder._mixins,

                done = false;

            if (!output) { // Assume parent will not provide the output array

                output = [];

                done = true;
            }

            if (mixins) {

                mixins.forEach(function (mixin) {

                    output.push(mixin);
                });
            }

            if (classBuilder._base) { // Not the end of the base chain

                getAllMixins(classBuilder._base, output);
            }

            if (done) {

                return output;
            }
        },

        createConstructor = function (classBuilder) {
            var ctor = classBuilder._constructor || classBuilder._base || function () { },

                mixins = classBuilder._mixins,

                initializer = classBuilder._initializer,

                constructor = function () { // Combine all the initialization steps in the constructor
                    var args = Array.prototype.slice.call(arguments);

                    // Call the constructor before calling the mixins
                    ctor.apply(this, args);

                    // Initialize the mixins
                    mixins.forEach(function (mixin) {

                        mixin.apply(this, args); // The mixin function acts as a constructor and initializer since the type is constructed at this point
                    },
                    this);

                    // At this moment the type and all the mixins have been constructed
                    // Apply the second initialization step in which the class can access the functionality provided by the mixins as well
                    if (initializer) {

                        //console.log('Calling ' + constructor.prototype.type + '::initialize on type: ' + this.type);

                        initializer.apply(this, args);
                    }
                };

            return constructor;
        },
        
        // Copies the members from "from" to "to".
        // The default action is to copy all the members without overwriting but there are options for ignoring items or overwriting then 
        // if they are specified in the ignore and overwrite arrays of the options
        copyMembers = function (from, to, options) {
            var ignore,
                overwrite;

            options = options || {};

            ignore = options.ignore || [];
            overwrite = options.overwrite || [];

            for (var name in from) {

                if (from.hasOwnProperty(name)) {

                    if (ignore.indexOf(name) > -1) { // It is in the ignore array of the configuration

                        continue;
                    }

                    if (to.hasOwnProperty(name) && // The member exists in the destination object
                        overwrite.indexOf(name) == -1 && // but it is not in the overwrite array in the options
                        overwrite.indexOf('*') == -1) { // and the overwrite "any" options was not specified

                        continue;
                    }

                    to[name] = from[name];
                }
            }
        };

    // Create the abstract base property builder
    PropertyBuilder = function () {

        this._propertyName = null;

        this._static = false; // Where to place the property in the created object (if static is false) or in the prototype (if static is true)

        this._configurable = false; // True if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object

        this._enumerable = false; // True if and only if this property shows up during enumeration of the properties on the corresponding object       
    };

    extend(PropertyBuilder, {

        setName: function (propertyName) {

            this._propertyName = type;

            return this;
        },

        isStatic: function () {

            this._static = flag;

            return this;
        },

        isConfigurable: function (flag) {

            this._configurable = flag;

            return this;
        },

        isEnumerable: function (flag) {

            this._enumerable = flag;

            return this;
        }

    });

    //typeManager.registerType(PropertyBuilder);

    // Create a property builder specific for data
    DataPropertyBuilder = function () {

        PropertyBuilder.call(this);

        this._value = undefined; // The value associated with the property. Can be any valid JavaScript value (number, object, function, etc)

        this._writable = false; // True if and only if the value associated with the property may be changed with an assignment operator
    };

    extend(DataPropertyBuilder, PropertyBuilder); // Inherit

    copyMembers({

        setValue: function (value) {

            this._value = value;

            return this;
        },

        isWritable: function (flag) {

            this._writable = flag;

            return this;
        },

        getDescriptor: function () {

            return {

                name: this._propertyName,

                'static': this._static,

                descriptor: {

                    configurable: this._configurable,

                    enumerable: this._enumerable,

                    // Data descriptor

                    value: this._value,

                    writable: this._writable
                }

            };
        }
    },
    DataPropertyBuilder);

    typeManager.registerType(DataPropertyBuilder);

    // Create a property builder specific for accessors 
    AccessorPropertyBuilder = function () {

        PropertyBuilder.call(this);

        this._get = undefined; // A function which serves as a getter for the property, or undefined if there is no getter. The function return will be used as the value of property

        this._set = undefined; // A function which serves as a setter for the property, or undefined if there is no setter. The function will receive as only argument the new value being assigned to the property
    };

    extend(AccessorPropertyBuilder, PropertyBuilder); // Inherit

    copyMembers({

        setGet: function (fcn) {

            this._get = fcn;

            return this;
        },

        setSet: function (fcn) {

            this._set = fcn;

            return this;
        },

        getDescriptor: function () {

            return {

                name: this._propertyName,

                'static': this._static,

                descriptor: {

                    configurable: this._configurable,

                    enumerable: this._enumerable,

                    // Accessor descriptor

                    get: this._get,

                    set: this._set
                }

            };
        }
    },
    AccessorPropertyBuilder);

    typeManager.registerType(AccessorPropertyBuilder);

    // Create the type builder
    ClassBuilder = function () {

        this._typeName = null;

        this._static = false;

        this._base = null;

        this._constructor = null;

        this._listeners = null; // Listeners to specific events of the object life cycle 

        this._mixins = null; // The mixins where the type gets functionality from

        this._initializer = null; // The initializer executes after all the mixins have their properties copied to the type

        this._methods = {};
    };

    extend(ClassBuilder, {

        setTypeName: function (type) {

            this._typeName = type;

            return this;
        },

        getTypeName: function () {

            return this._typeName;
        },

        isStatic: function (flag) {

            this._static = flag;

            return this;
        },

        setBase: function (base) {

            this._base = base;

            return this;
        },

        getBase: function () {

            return this._base;
        },

        setConstructor: function (constructor) {

            this._constructor = constructor;

            return this;
        },

        getConstructor: function () {

            return this._constructor;
        },

        addListener: function (name, listener) {

            if (!name) {

                throw new Error('Name is required in addListener');
            }

            if (!listener) {

                throw new Error('Listener is required in addListener');
            }

            if (this._listeners && this._listeners[name]) {

                throw new Error('Listener: ' + name + ' already exists');
            }

            this._listeners = this._listeners || {};

            this._listeners[name] = listener;

            return this;
        },

        setListener: function (name, listener) {

            if (!name) {

                throw new Error('Name is required in setListener');
            }

            if (!listener) {

                throw new Error('Listener is required in setListener');
            }

            this._listeners = this._listeners || {};

            this._listeners[name] = listener;

            return this;
        },

        addMixin: function (name, mixin) {

            if (!name) {

                throw new Error('Name is required in addMixin');
            }

            if (!mixin) {

                throw new Error('Mixin is required in addMixin');
            }

            if (this._mixins && this._mixins[name]) {

                throw new Error('Mixin: ' + name + ' already exists');
            }

            this._mixins = this._mixins || {};

            this._mixins[name] = mixin;

            return this;
        },

        setMixin: function (name, mixin) {

            if (!name) {

                throw new Error('Name is required in setMixin');
            }

            if (!mixin) {

                throw new Error('Mixin is required in setMixin');
            }

            this._mixins = this._mixins || {};

            this._mixins[name] = mixin;

            return this;
        },

        setInitializer: function (initializer) {

            this._initializer = initializer;

            return this;
        },

        getInitializer: function () {

            return this._initializer;
        },

        addProperty: function (propertyBuilder) {

            if (!propertyBuilder) {

                throw new Error('PropertyBuilder is required in addProperty');
            }

            if (this._properties[name]) {

                throw new Error('Property: ' + name + ' already exists');
            }

            this._properties[name] = propertyBuilder;

            return this;
        },

        setProperty: function (propertyBuilder) {

            if (!propertyBuilder) {

                throw new Error('PropertyBuilder is required in setProperty');
            }

            this._properties[name] = propertyBuilder;

            return this;
        },

        addFunction: function (name, fcn) {

            if (!name) {

                throw new Error('Name is required in addFunction');
            }

            if (!fcn) {

                throw new Error('Function is required in addFunction');
            }

            return this.addProperty(

                new DataPropertyBuilder()

                    .isStatic(true) // All the functions are static by default so they go to the prototype

                    .setName(name)

                    .setValue(fcn)
            );
        },

        setFunction: function (name, fcn) {

            if (!name) {

                throw new Error('Name is required in setFunction');
            }

            if (!fcn) {

                throw new Error('Function is required in setFunction');
            }

            return this.setProperty(

                new DataPropertyBuilder()

                    .isStatic(true) // All the functions are static by default so they go to the prototype

                    .setName(name)

                    .setValue(fcn)
            );
        },

        addField: function (name, field) {

            if (!name) {

                throw new Error('Name is required in addField');
            }

            return this.addProperty(

                new DataPropertyBuilder()

                    .isStatic(true) // All the fields are static by default so they go to the prototype, the nono static ones can be set in the constructor

                    .setName(name)

                    .setValue(field)
            );
        },

        setField: function (name, field) {

            if (!name) {

                throw new Error('Name is required in setField');
            }

            return this.setProperty(

                new DataPropertyBuilder()

                    .isStatic(true) // All the fields are static by default so they go to the prototype, the nono static ones can be set in the constructor

                    .setName(name)

                    .setValue(fi)
            );
        },

        onTypeDefined: function(fcn) {

            this._onTypeDefined = fcn;

            return this;
        },

        // Builds the type
        define: function () {
            var constructor = createConstructor(this),

                base = types.isString(this._base) ? typeManager.getType(this._base) : this._base,

                allMixins = getAllMixins(this),
                    
                instance,
                    
                constructorOrInstance;

            // If there is a base, then extendi it
            if (base) {

                extend(constructor, base);
            }

            // Add any object life cycle handler to the constructor
            if (this._listeners) {

                constructor.listeners = this._listeners;
            }

            // Add the properties
            copyMembers(this._methods, constructor.prototype);
            
            // Copy the members of the mixins to the prototype of the type
            if (this._mixins) {

                this._mixins.forEach(function (mixin) {

                    copyMembers(mixin.prototype,
                        constructor.prototype,
                        {
                            ignore: ['type']
                        });
                });
            }
           
            // Add a function to the constructor to allow notifying the mixins when an event in the object life cycle occurs
            constructor.notifyMixins = function (listenerName, scope, cfg) {
                var listener;

                allMixins.forEach(function (mixin) {

                    if (mixin.listeners) {

                        listener = mixin.listeners[listenerName];

                        if (listener) {

                            listener.call(scope, cfg);
                        }
                    }
                });
            };

            // If the type is static, then create a single instance
            if (this._static) {

                instance = new constructor();            
            }

            constructorOrInstance = instance || constructor;

            // Static initialization per module after the type is created
            if (this._onTypeDefined) {

                this._onTypeDefined(constructorOrInstance);
            }

            // Self register if a type is provided
            if (this._typeName) {

                typeManager.registerType(constructorOrInstance);
            }

            return constructorOrInstance;
        },

        defineOnce: function () {
            var constructorOrInstance;

            if (!this._typeName) {

                throw Error('The type name is required in defineOnce');
            }

            constructorOrInstance = typeManager.getType(type, true); // Optional

            if (constructorOrInstance) { // It has been already loaded

                return constructorOrInstance;
            }

            if (this._onBeforeTypeDefined) { // Static initialization per module before the type is created

                this._onBeforeTypeDefined();
            }

            return this.define(); // It self registers
        },
    });

    typeManager.registerType(ClassBuilder);

    return ClassBuilder;
});
